Lambda無しでもいけます!!許可されたサイトにだけCORSを許可するためにAPI Gatewayのモックレスポンスを動的に設定してみた
CX事業本部@大阪の岩田です。現在開発中の案件でAPI GatewayからのレスポンスヘッダAccess-Control-Allow-Headers
を動的に設定したいという要件があり、API Gatewayのマッピングテンプレートを使って要件を実現しました。マッピングテンプレートを記述するためのVTLに関して情報があまり見つからなかったので、対応した内容についてブログにまとめてみました。
やりたかったこと
現在開発中の案件ではSPAからAPI Gateway × Lambdaで構築したREST APIを呼び出すというよくある構成で開発を進めています。開発中のSPAは
- 開発者のローカルマシン
- AWS上の開発環境(S3やCloudFront)
といった複数の環境で稼働するため、開発環境に関してはAPI GatewayAccess-Control-Allow-Origin: *
を返却することでクロスドメインのアクセスを許可していたのですが(本番環境はちゃんと絞ります...)、特殊な要件から、あるAPIに関してはSet-Cookie
を返却してブラウザにCookieを保存する必要が出てきました。JavaScriptからのAPI呼び出しでCookieをセットするには
- SPAからAPIを呼び出す際に適切なオプションを付与する
※今回はReact×Amplifyの構成だったのでAPI呼び出し時に
{withCredentials:true}
を付与しました - サーバーサイドから返却するレスポンスヘッダを以下のように変更する
Set-Cookie
でSameSite
属性を適切に設定する ※Chrome80におけるデフォルト動作の変更対策Access-Control-Allow-Credentials: True
を返却するAccess-Control-Allow-Origin
に*以外を返却する
といった対応が必要になります。そうなんですAccess-Control-Allow-Origin: *
は使えないんです。かといって決め打ちでhttp://localhost:3000
のような指定もしたくありません。開発者ごとに環境は違うので、別PJとの兼ね合いでポート番号を変更したいケースも考えられますし、ループバックアドレスを使いたいかもしれません。AWS環境にデプロイしてテストする際にも困ります。
ということで...特定のオリジンに対してだけCORSを許可するよう動的にAccess-Control-Allow-Origin
を返却する必要が出てきました。オリジンがhttp://localhost:3000
の場合はAccess-Control-Allow-Origin: http://localhost:3000
を返却し、オリジンがhttp://example.com
の場合はAccess-Control-Allow-Origin
を返却しない。といった具合です。
このような処理を実現するためにはアプリ側で許可されたオリジンの一覧を保持しておき、リクエストヘッダのoriginと突き合わせて動的にレスポンスを生成する手法が知られています。
API Gatewayのバックエンドで起動するLambdaにはこのような処理を組み込む想定ではいたのですが、API Gatewayの構成は以下のようになっていました。
/api |---OPTIONS...統合タイプ:モック |---GET...統合タイプ:Lambda関数
OPTIONSメソッドについては統合タイプがモックになっておりLambdaでロジックを書けないのです。OPTIONSメソッドの統合タイプをLambda関数に変更して、Access-Control-Allow-Origin
を動的に生成するだけのLambdaを用意することも考えたのですが、レスポンスヘッダ1個のためにLambdaを用意するのもちょっと大げさな気がします。できればLambda無しでOPTIONSメソッドのレスポンスを動的に生成したいところです。
そこでマッピングテンプレート!!
ここで登場するのがAPI Gatewayのマッピングテンプレートです。マッピングテンプレートを利用することでAPIのリクエスト/レスポンスパラメータ及びステータスコードをオーバーライドすることが可能です。さっそく設定してみましょう。まずはマネコンから「CORSの有効化」を選択し、OPTIONSメソッドの設定を用意します。
デフォルト設定のままCORSを有効化すると、以下のようにメソッドレスポンスが設定されます。レスポンスヘッダにAccess-Control-Allow-Origin
が存在することを確認しておきましょう。※後で削除します。
次に統合レスポンスの設定です。ここの設定でレスポンスヘッダAccess-Control-Allow-Origin
に実際にマッピングする値を定義します。マッピングテンプレートの定義に以下のように記述します。
#set($origin = $input.params().header.get('origin')) #if ($origin.matches("^http(s?)://(localhost|127\.0\.0\.1|.*\.cloudfront\.net|s3-\.*-\d?\.amazonaws\.com)(:?\d+)?")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end
やっていることはすごくシンプルで、以下の通りです。
- まず
#set($origin = $input.params().header.get('origin'))
の部分でリクエストヘッダのorigin
を取得して$origin
という変数にセットします。 - 続いて
#if ($origin.matches(...
の部分で$origin
にセットされた文字列が許可されたオリジンかどうかを判定しています。とりあえずlocalhostと127.0.0.1、CloudFront全般とS3全般を許可しています。もう少し厳密にやるならCloudFrontやS3のドメインを絞ると良いでしょう。 - if文の評価にマッチした場合はリクエストヘッダの
$context.responseOverride.header.Access-Control-Allow-Origin
に変数$origin
をセットします。$context.responseOverride.header.<ヘッダ名>
という変数をセットすることでレスポンスヘッダ内の指定したヘッダの値を上書くことが可能です。
マッピングテンプレートを使用して、API のリクエストおよびレスポンスパラメータとステータスコードをオーバーライドする
マネコン上の表示はこんな感じになります。
最後にメソッドレスポンスからAccess-Control-Allow-Origin
を消しておきましょう。これをやっておかないと、許可されたオリジン以外からのアクセスがあった場合(マッピングテンプレートのIF文にマッチしなかった場合)にAccess-Control-Allow-Origin: *
が返却されてしまいます。最初から消しておけば良いのですが、そうするとマネコンからマッピングテンプレートを保存する際にInvalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression parameter specified: method.response.header.Access-Control-Allow-Origin]
というエラーが出てしまうので、マッピングテンプレートの保存後に削除しています。
やってみる
ここまでで準備ができたので、動作を確認してみます。
まずオリジンを指定しない場合...
$curl -XOPTIONS https://<API GWのID>.execute-api.ap-northeast-1.amazonaws.com/dev -i HTTP/2 200 date: Thu, 01 Oct 2020 08:54:15 GMT content-type: application/json content-length: 1 x-amzn-requestid: 08927537-4f43-40ac-a952-28ab101627cf access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token x-amz-apigw-id: TuTgnHdqNjMFa1g= access-control-allow-methods: OPTIONS
Access-Control-Allow-Origin
は返ってきません。
続いて許可されたオリジンを指定した場合...
$curl -XOPTIONS https://<API GWのID>.execute-api.ap-northeast-1.amazonaws.com/dev -i -H "origin: http://localhost:3000" HTTP/2 200 date: Thu, 01 Oct 2020 08:55:23 GMT content-type: application/json content-length: 1 x-amzn-requestid: e8aa81dd-37c4-43ca-acfa-c8f103f0c782 access-control-allow-origin: http://localhost:3000 access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token x-amz-apigw-id: TuTrXFSntjMFpgA= access-control-allow-methods: OPTIONS
今度はAccess-Control-Allow-Origin: http://localhost:3000
が返却されてきました。
最後に許可されていないオリジンhttps://example.com
を指定した場合です
$ curl -XOPTIONS https://<API GWのID>.execute-api.ap-northeast-1.amazonaws.com/dev -i -H "origin: https://example.com" HTTP/2 200 date: Thu, 01 Oct 2020 08:57:13 GMT content-type: application/json content-length: 1 x-amzn-requestid: dda0e4a0-4583-466a-9625-51759811b36f access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token x-amz-apigw-id: TuT8bHHAtjMFVlQ= access-control-allow-methods: OPTIONS
こちらもAccess-Control-Allow-Origin
は返ってきません。意図通り動作していそうです。
まとめ
API Gatewayのマッピングテンプレートを使って動的にレスポンスヘッダーを調整する例をご紹介しました。マッピングテンプレートで利用するVelocity Template Language (VTL)に関してネット上にもあまり情報が転がっておらず、今回利用した正規表現でのチェックmatches
も「多分できるけんだろうけど書き方が分からない...」という状態になってしまったので、同じような境遇の人のためになればと思いブログ化してみました。VTLはAppSyncでも利用する記法なので、この機会にちゃんと勉強してみようと思います。